using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.Text;
using Savage.DotNetLibrary.ProtocolLibrary;

namespace Savage.DotNetLibrary.ImageLibrary
{
    /// <summary>
    /// Creates an object that can extract the Exif properties
    /// </summary>
    public class Exif
    {
        /// <summary>
        /// Instantiates a new Exif object that reads the Exif values for the provided image.
        /// </summary>
        /// <param name="image">the image to read the Exif properties of.</param>
        public Exif(Bitmap image)
        {
            if (image == null)
                throw new ArgumentNullException("image");

            _bitmap = image;
            _propertyIdList = _bitmap.PropertyIdList;

            //Load the GpsData if it exists
            if (PropertyExists(ExifProperty.GPSVersionId))
            {
                ReadGpsTagData();
            }
        }

        ~Exif()
        {
            if (_bitmap!=null)
                _bitmap.Dispose();
        }

        private void ReadGpsTagData()
        {
            _gpsData = new ProtocolLibrary.GPS.GPSData();
            //Load the version id
            _gpsData.VersionId = GetExifBytesValue(ExifProperty.GPSVersionId);
            
            //Latitude
            string latitudeRef = GetExifStringValue(ExifProperty.GPSLatitudeRef);
            ProtocolLibrary.GPS.GPSReference gpsLatitudeRef = (ProtocolLibrary.GPS.GPSReference)Enum.Parse(typeof(ProtocolLibrary.GPS.GPSReference), latitudeRef.Substring(0, 1));
            ExifRational[] gpsLatitude = GetExifRationalValue(ExifProperty.GPSLatitude);
            int degreesLat = (int)gpsLatitude[0].ToDecimal();
            int minutesLat = (int)gpsLatitude[1].ToDecimal();
            decimal secondsLat = gpsLatitude[2].ToDecimal();

            _gpsData.Latitude = new ProtocolLibrary.GPS.GPSPosition(degreesLat, minutesLat, secondsLat, gpsLatitudeRef);

            //Longitude
            string longitudeRef = GetExifStringValue(ExifProperty.GPSLongitudeRef);
            ProtocolLibrary.GPS.GPSReference gpsLongitudeRef = (ProtocolLibrary.GPS.GPSReference)Enum.Parse(typeof(ProtocolLibrary.GPS.GPSReference), longitudeRef.Substring(0, 1));
            ExifRational[] gpsLongitude = GetExifRationalValue(ExifProperty.GPSLongitude);
            int degreesLong = (int)gpsLongitude[0].ToDecimal();
            int minutesLong = (int)gpsLongitude[1].ToDecimal();
            decimal secondsLong = gpsLongitude[2].ToDecimal();
            _gpsData.Longitude = new ProtocolLibrary.GPS.GPSPosition(degreesLong, minutesLong, secondsLong, gpsLongitudeRef);

            //Altitude data
            byte[] altitudeRef = GetExifBytesValue(ExifProperty.GPSAltitudeRef);

            switch (altitudeRef[0])
            {
                case 0:
                    _gpsData.AltitudeRef = ProtocolLibrary.GPS.GPSAltitudeRef.SeaLevelOrAbove;
                    break;
                case 1:
                    _gpsData.AltitudeRef = ProtocolLibrary.GPS.GPSAltitudeRef.BelowSeaLevel;
                    break;
                default:
                    _gpsData.AltitudeRef = ProtocolLibrary.GPS.GPSAltitudeRef.Other;
                    break;
            }

            ExifRational[] altitude = GetExifRationalValue(ExifProperty.GPSAltitude);
            _gpsData.Altitude = altitude[0].ToDecimal();
            
            //Satellites
            _gpsData.Satellites = GetExifStringValue(ExifProperty.GPSSatellites);

            //GPS Status
            _gpsData.Status = ProtocolLibrary.GPS.GPSStatus.NotSpecified;
            string status = GetExifStringValue(ExifProperty.GPSStatus);
            if (!String.IsNullOrEmpty(status))
            {
                switch (status.ToUpperInvariant())
                {
                    case "A":
                        _gpsData.Status = ProtocolLibrary.GPS.GPSStatus.InProgress;
                        break;
                    case "V":
                        _gpsData.Status = ProtocolLibrary.GPS.GPSStatus.Interoperability;
                        break;
                    default:
                        _gpsData.Status = ProtocolLibrary.GPS.GPSStatus.Other;
                        break;
                }
            }

            //MeasureMode
            string measureMode = GetExifStringValue(ExifProperty.GPSMeasureMode);
            _gpsData.MeasureMode = ProtocolLibrary.GPS.GPSMeasureMode.NotSpecified;
            if (!String.IsNullOrEmpty(measureMode))
            {
                switch (measureMode)
                {
                    case "2":
                        _gpsData.MeasureMode = ProtocolLibrary.GPS.GPSMeasureMode.TwoDimensional;
                        break;
                    case "3":
                        _gpsData.MeasureMode = ProtocolLibrary.GPS.GPSMeasureMode.ThreeDimensional;
                        break;
                    default:
                        _gpsData.MeasureMode = ProtocolLibrary.GPS.GPSMeasureMode.Other;
                        break;
                }
            }

            //MapDatum
            _gpsData.MapDatum = GetExifStringValue(ExifProperty.GPSMapDatum);

            //DateTime Stamp
            string dateStamp = GetExifStringValue(ExifProperty.GPSDateStamp);
            if (!String.IsNullOrEmpty(dateStamp))
            {
                string dateTimeStamp = dateStamp;
                
                ExifRational[] timeStamp = GetExifRationalValue(ExifProperty.GPSTimeStamp);
                if (timeStamp != null && timeStamp.Length == 3)
                {
                    string hour = timeStamp[0].ToDecimal().ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
                    string minute = timeStamp[1].ToDecimal().ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
                    string seconds = timeStamp[2].ToDecimal().ToString(CultureInfo.InvariantCulture).PadLeft(2, '0');
                    dateTimeStamp += " " + hour + ":" + minute + ":" + seconds;
                }
                else
                {
                    dateTimeStamp += " 00:00:00";  
                }
                _gpsData.UtcDateTimeStamp = ExifDateParse(dateTimeStamp);
            }
        }

        private Bitmap _bitmap;
        private int[] _propertyIdList;

        private bool PropertyExists(ExifProperty property)
        {
            return (Array.IndexOf(_propertyIdList, (int)property) >= 0);
        }

        private PropertyItem GetPropertyItem(ExifProperty property)
        {
            if (PropertyExists(property))
            {
                return _bitmap.GetPropertyItem((int) property);
            }
            else
            {
                return null;
            }
        }

        private string GetExifStringValue(ExifProperty property)
        {
            PropertyItem item = GetPropertyItem(property);
            
            if (item == null)
                return null;

            byte[] rawData = item.Value;
            return Encoding.ASCII.GetString(rawData).TrimEnd(new char[] { '\0' });
        }

        private byte[] GetExifBytesValue(ExifProperty property)
        {
            PropertyItem item = GetPropertyItem(property);
            if (item == null)
                return null;

            return item.Value;  
        }

        protected short? GetExifShortValue(ExifProperty property)
        {
            PropertyItem item = GetPropertyItem(property);
            
            if (item == null)
                return null;

            byte[] rawData = item.Value;
            return BitConverter.ToInt16(rawData, 0);
        }

        protected short[] GetExifShortArrayValue(ExifProperty property)
        {
            PropertyItem item = GetPropertyItem(property);
            
            if (item == null)
                return null;

            byte[] rawData = item.Value;
                
            List<short> values= new List<short>();
            for(int i=0;i<=rawData.Length-2;i=i+2)
            {
                    values.Add(BitConverter.ToInt16(rawData, i)); 
            }
            return values.ToArray();
        }
        
        protected int? GetExifLongValue(ExifProperty property)
        {
            if (PropertyExists(property))
            {
                byte[] rawData = _bitmap.GetPropertyItem((int)property).Value;
                return BitConverter.ToInt32(rawData, 0);
            }
            return null;
        }

        protected ExifRational[] GetExifRationalValue(ExifProperty property)
        {
            if (PropertyExists(property))
            {
                byte[] rawData = _bitmap.GetPropertyItem((int)property).Value;

                List<ExifRational> values = new List<ExifRational>();
                for (int i = 0; i <= rawData.Length - 8; i = i + 8)
                {
                    int numerator = BitConverter.ToInt32(rawData, i);
                    int denominator = BitConverter.ToInt32(rawData, i+4);

                    values.Add(new ExifRational(numerator, denominator));
                }
                
                return values.ToArray();
            }
            return null;
        }

        private static DateTime? ExifDateParse(string dateString)
        {
            if (dateString == null)
                return null;

            if (!System.Text.RegularExpressions.Regex.IsMatch(dateString, @"[0-9]{4}:[0-9]{2}:[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}"))
                return null;
            
            int spaceIndex = dateString.IndexOf(" ", StringComparison.Ordinal);
            string result = dateString.Substring(0, spaceIndex).Replace(":", "-") + dateString.Substring(spaceIndex, 9);
            
            return DateTime.Parse(result, CultureInfo.InvariantCulture);
        }

        public void IterateProperties()
        {
            foreach(int i in _propertyIdList)
            {
                byte[] rawData = _bitmap.GetPropertyItem(i).Value;
            }
        }

        //TIFF Tev. 6.0 Tags

        #region "TIFF a) Image Data Structure"

        /// <summary>
        /// The number of columns of image data, equal to the number of pixels per row. In JPEG compressed data a JPEG marker is used instead of this tag.
        /// </summary>
        public int? ImageWidth
        {
            get { return GetExifLongValue(ExifProperty.ImageWidth); }
        }

        /// <summary>
        /// The number of rows of image data. In JPEG compressed data a JPEG marker is used instead of this tag.
        /// </summary>
        public int? ImageHeight
        {
            get { return GetExifLongValue(ExifProperty.ImageHeight); }
        }

        /// <summary>
        /// The number of bits per image component. In this standard each component of the image is 8 bits, so the value for this tag is 8. See also SamplesPerPixel. In JPEG compressed data a JPEG marker is used instead of this tag.
        /// </summary>
        public short[] BitsPerSample
        {
            get { return GetExifShortArrayValue(ExifProperty.BitsPerSample); }
        }

        /// <summary>
        /// The compression scheme used for the image data. When a primary image is JPEG compressed, this designation is not necessary and is omitted.
        /// </summary>
        public TiffCompression Compression
        {
            get
            {
                short? value = GetExifShortValue(ExifProperty.Compression);

                if (value != null)
                    return (TiffCompression)value;
                else
                    return TiffCompression.NotSpecified;
            }
        }

        /// <summary>
        /// The pixel composition. In JPEG compressed data a JPEG marker is used instead of this tag.
        /// </summary>
        public TiffPhotometricInterpolation PhotometricInterpolation
        {
            get
            {
                short? value = GetExifShortValue(ExifProperty.PhotometricInterpolationMode);
                if (value != null)
                    return (TiffPhotometricInterpolation)value;
                else
                    return TiffPhotometricInterpolation.NotSpecified;
            }
        }

        /// <summary>
        /// The image orientation viewed in terms of rows and columns. Returns the position of row 0 and column 0.
        /// </summary>
        public TiffOrientation Orientation
        {
            get
            {
                short? value = GetExifShortValue(ExifProperty.Orientation);
                if (value != null)
                    return (TiffOrientation)value;
                else
                    return TiffOrientation.NotSpecified;
            }
        }

        /// <summary>
        /// The number of components per pixel. Since this standard applies to RGB and YCbCr images, the value set for this tag is 3. In JPEG compressed data a JPEG marker is used instead of this tag.
        /// </summary>
        public short? SamplesPerPixel
        {
            get { return GetExifShortValue(ExifProperty.SamplesPerPixel); }
        }

        /// <summary>
        /// Indicates whether pixel components are recorded in chunky or planar format. In JPEG compressed files a JPEG marker is used instead of this tag. If this field does not exist, the TIFF default of 1 (chunky) is assumed.
        /// </summary>
        public TiffPlanarConfiguration PlanarConfiguration
        {
            get
            {
                short? value = GetExifShortValue(ExifProperty.PlanarConfiguration);
                if (value != null)
                    return (TiffPlanarConfiguration)value;
                else
                    return TiffPlanarConfiguration.NotSpecified;
            }
        }

        /// <summary>
        /// The sampling ratio of chrominance components in relation to the luminance component. In JPEG compressed data a JPEG marker is used instead of this tag.
        /// </summary>
        public short? YCbCrSubSampling
        {
            get { return GetExifShortValue(ExifProperty.YCbCrSubSampling); }
        }

        /// <summary>
        /// The position of chrominance components in relation to the luminance component.
        /// </summary>
        public TiffYCbCrPosition YCbCrPositioning
        {
            get
            {
                short? value = GetExifShortValue(ExifProperty.YCbCrPositioning);
                if (value != null)
                    return (TiffYCbCrPosition)value;
                else
                    return TiffYCbCrPosition.NotSpecified;
            }
        }

        /// <summary>
        /// The number of pixels per ResolutionUnit in the ImageWidth direction. When the image resolution is unknown, 72 [dpi] is designated.
        /// </summary>
        public ExifRational XResolution
        {
            get { return GetExifRationalValue(ExifProperty.XResolution)[0]; }
        }

        /// <summary>
        /// The number of pixels per ResolutionUnit in the ImageLength direction. The same value as XResolution is designated.
        /// </summary>
        public ExifRational YResolution
        {
            get { return GetExifRationalValue(ExifProperty.YResolution)[0]; }
        }

        /// <summary>
        /// The unit for measuring XResolution and YResolution. The same unit is used for both XResolution and YResolution.
        /// </summary>
        public TiffResolutionUnit ResolutionUnit
        {
            get
            {
                short? value = GetExifShortValue(ExifProperty.ResolutionUnit);
                if (value == null)
                    return TiffResolutionUnit.NotSpecified;

                switch (value)
                {
                    case 2:
                        return TiffResolutionUnit.Inches;
                    case 3:
                        return TiffResolutionUnit.Centimetres;
                    default:
                        return TiffResolutionUnit.Other;
                }
            }
        }

        #endregion

        #region "TIFF b) Recording offset"

        /// <summary>
        /// For each strip, the byte offset of that strip. It is recommended that this be selected so the number of strip bytes does not exceed 64 Kbytes. With JPEG compressed data this designation is not needed and is omitted. See also RowsPerStrip and StripByteCounts.
        /// </summary>
        public int? StripOffsets
        {
            get { return GetExifLongValue(ExifProperty.StripOffsets); }
        }

        /// <summary>
        /// The number of rows per strip. This is the number of rows in the image of one strip when an image is divided into strips. With JPEG compressed data this designation is not needed and is omitted. See also RowsPerStrip and StripByteCounts.
        /// </summary>
        public int? RowsPerStrip
        {
            get { return GetExifLongValue(ExifProperty.RowsPerStrip); }
        }

        /// <summary>
        /// The total number of bytes in each strip. With JPEG compressed data this designation is not needed and is omitted.
        /// </summary>
        public int? StripByteCounts
        {
            get { return GetExifLongValue(ExifProperty.StripByteCounts); }
        }

        /// <summary>
        /// The offset to the start byte (SOI) of JPEG compressed thumbnail data. This is not used for primary image JPEG data.
        /// </summary>
        public int? JpegInterchangeFormat
        {
            get { return GetExifLongValue(ExifProperty.JpegInterchangeFormat); }
        }

        /// <summary>
        /// The number of bytes of JPEG compressed thumbnail data. This is not used for primary image JPEG data.
        /// </summary>
        public int? JpegInterchangeFormatLength
        {
            get { return GetExifLongValue(ExifProperty.JpegInterchangeFormatLength); }
        }

        #endregion

        //TODO: Add TIFF Section C

        #region "TIFF d) Other tags"
        /// <summary>
        /// The date and time of image creation. In this standard it is the date and time the file was changed.
        /// </summary>
        public DateTime? FileChangeDateTime
        {
            get
            {
                if (PropertyExists(ExifProperty.FileChangeDateTime))
                    return ExifDateParse(GetExifStringValue(ExifProperty.FileChangeDateTime));
                else
                    return null;
            }
        }

        /// <summary>
        /// A character string giving the title of the image. It may be a comment such as "1988 company picnic" or the like.
        /// </summary>
        public string ImageDescription
        {
            get { return GetExifStringValue(ExifProperty.ImageDescription); }
        }

        /// <summary>
        /// The manufacturer of the recording equipment. This is the manufacturer of the DSC, scanner, video digitizer or other equipment that generated the image. When the field is left blank, it is treated as unknown.
        /// </summary>
        public string Make
        {
            get { return GetExifStringValue(ExifProperty.Make); }
        }

        /// <summary>
        /// The model name or model number of the equipment. This is the model name of number of the DSC, scanner, video digitizer or other equipment that generated the image. When the field is left blank, it is treated as unknown.
        /// </summary>
        public string Model
        {
            get { return GetExifStringValue(ExifProperty.Model); }
        }

        /// <summary>
        /// This tag records the name and version of the software or firmware of the camera or image input device used to generate the image. When the field is left blank, it is treated as unknown.
        /// </summary>
        public string Software
        {
            get { return GetExifStringValue(ExifProperty.Software); }
        }

        /// <summary>
        /// This tag records the name of the camera owner, photographer or image creator. When the field is left blank, it is treated as unknown.
        /// </summary>
        public string Artist
        {
            get { return GetExifStringValue(ExifProperty.Artist); }
        }

        /// <summary>
        /// Copyright information. In this standard the tag is used to indicate both the photographer and editor copyrights. It is the copyright notice of the person or organization claiming rights to the image.
        /// </summary>
        public string Copyright
        {
            get { return GetExifStringValue(ExifProperty.Copyright); }
        }
        #endregion

        //Exif IFD Tags

        #region "Exif IFD a) Version"
        /// <summary>
        /// The version of this standard supported. Nonexistence of this field is taken to mean nonconformance to the standard. Conformance to this standard is indicated by recording "0220" as 4-byte ASCII.
        /// </summary>
        public string ExifVersion
        {
            get { return GetExifStringValue(ExifProperty.ExifVersion); }
        }

        /// <summary>
        /// The Flashpix format version supported by a FPXR file. If the FPXR function supports Flashpix format Ver. 1.0, this is indicated similarly to ExifVersion by recording "0100" as 4-byte ASCII.
        /// </summary>
        public string FlashpixVersion
        {
            get { return GetExifStringValue(ExifProperty.FlashpixVersion); }
        }
        #endregion

        #region "Exif IFD b) Color Space"

        /// <summary>
        /// The color space information tag (ColorSpace) is always recorded as the color space specifier.
        /// </summary>
        public ExifColorSpace ColorSpace
        {
            get
            {
                int? value = GetExifShortValue(ExifProperty.ColorSpace);
                if (value == null)
                    return ExifColorSpace.NotSpecified;

                switch (value)
                { 
                    case 0x01:
                        return ExifColorSpace.sRGB;
                    case 0xFFFF:
                        return ExifColorSpace.Uncalibrated;
                    default:
                        return ExifColorSpace.Other;
                }
            }
        }
        #endregion

        #region "Exif IFD c) Image Configuration"

        /// <summary>
        /// Information specific to compressed data. When a compressed file is recorded, the valid width of the meaningful image shall be recorded in this tag, whether or not there is padding data or a restart marker. This tag should not exist in an uncompressed file.
        /// </summary>
        public short? PixelXDimension
        {
            get { return GetExifShortValue(ExifProperty.PixelXDimension); }
        }

        /// <summary>
        /// Information specific to compressed data. When a compressed file is recorded, the valid height of the meaningful image shall be recorded in this tag, whether or not there is padding data or a restart marker. This tag should not exist in an uncompressed file. Since data padding is unnecessary in the vertical direction, the number of lines recorded in this valid image height tag will in fact be the same as that recorded in the SOF.
        /// </summary>
        public short? PixelYDimension
        {
            get { return GetExifShortValue(ExifProperty.PixelYDimension); }
        }

        //TODO: ComponentsConfiguration

        //TODO: CompressedBitsPerPixel
        #endregion

        #region "Exif IFD d) User Information"
        /// <summary>
        /// A tag for manufacturers of Exif writers to record any desired information. The contents are up to the manufacturer, but this tag should not be used for any other than its intended purpose.
        /// </summary>
        public string MakerNote
        {
            get { return GetExifStringValue(ExifProperty.MakerNote); }
        }

        /// <summary>
        /// A tag for Exif users to write keywords or comments on the image besides those in ImageDescription, and without the character code limitations of the ImageDescription tag.
        /// </summary>
        public string UserComment
        {
            get { return GetExifStringValue(ExifProperty.UserComment); }
        }

        #endregion

        //TODO : Add Exif IFD Section E

        #region "Exif IFD f) Date and Time"

        /// <summary>
        /// The date and time when the original image data was generated. For a DSC the date and time the picture was taken are recorded.
        /// </summary>
        public DateTime? DateTimeOriginal
        {
            get { return ExifDateParse(GetExifStringValue(ExifProperty.DateTimeOriginal)); }
        }

        /// <summary>
        /// The date and time when the image was stored as digital data. If, for example, an image was captured by DSC and at the same time the file was recorded, then the DateTimeOriginal and DateTimeDigitized will have the same contents.
        /// </summary>
        public DateTime? DateTimeDigitized
        {
            get { return ExifDateParse(GetExifStringValue(ExifProperty.DateTimeDigitized)); }
        }

        /// <summary>
        /// A tag used to record fractions of seconds for the DateTime tag.
        /// </summary>
        public string SubsecTime
        {
            get { return GetExifStringValue(ExifProperty.SubsecTime); }
        }

        /// <summary>
        /// A tag used to record fractions of seconds for the DateTimeOriginal tag.
        /// </summary>
        public string SubsecOriginal
        {
            get { return GetExifStringValue(ExifProperty.SubsecOriginal); }
        }

        /// <summary>
        /// A tag used to record fractions of seconds for the DateTimeDigitized tag.
        /// </summary>
        public string SubsecDigitized
        {
            get { return GetExifStringValue(ExifProperty.SubsecDigitized); }
        }

        #endregion

        //TODO : Add Exif IFD Section G

        /// <summary>
        /// The class of the program used by the camera to set exposure when the picture is taken.
        /// </summary>
        public ExifExposureProgram ExposureProgram
        {
            get 
            {
                short? value = GetExifShortValue(ExifProperty.ExposureProgram);
                if (value == null)
                    return ExifExposureProgram.NotDefined;

                return (ExifExposureProgram)value; }
        }

        #region "Exif IFD h) Other Tags"

        /// <summary>
        /// This tag indicates an identifier assigned uniquely to each image. It is recorded as an ASCII string equivalent to hexadecimal notation and 128-bit fixed length.
        /// </summary>
        public string ImageUniqueId
        {
            get { return GetExifStringValue(ExifProperty.ImageUniqueId); }
        }

        #endregion

        //GPS
        private ProtocolLibrary.GPS.GPSData _gpsData;
        /// <summary>
        /// Contains the tags for GPS IFD
        /// </summary>
        public ProtocolLibrary.GPS.GPSData GPSData
        {
            get { return _gpsData; }
        }
    }
}
